Обновить

Native https implementation using crypto++ for I2P bootstrapping

Время на прочтение 7 min
Количество просмотров 14K
Each new I2P node, when first launched, must receive an initial list of nodes from somewhere. For this purpose, there are special servers (reseed), the addresses of which are hardcoded in the code. Previously, downloads were carried out via http, but recently reseeds began to switch to https. For successful work «purple" I2P it was also necessary to make appropriate changes. The cryptographic library used there crypto++ does not support ssl. Instead of using an additional library like openssl, which actually duplicates cryptography, the option discussed below was chosen.
Bootstrap is the only place in I2P where https is used.
On the other hand, the article will be interesting to those who are interested in understanding how ssl works and trying it themselves.



Reinventing the wheel


Our goal is to obtain the i2pseeds.su3 file, about 100K in size, from one of the reseed I2P nodes. This file is signed with a separate certificate, independent of the host certificate, so certificate verification can be eliminated. The relatively short length of the received data allows us not to implement mechanisms for compression and restoration of broken connections.
Will be used exclusively TLS 1.2 and the TLS_RSA_WITH_AES_256_CBC_SHA256 cipher suite. In other words, AES256 in CBC mode is used to encrypt data, and RSA is used for key negotiation.
This choice is due to the fact that AES256-CBC is the most used encryption in I2P, and RSA to simplify the implementation of the protocol by reducing the number of messages required for key negotiation. In addition to RSA and AES, you will also need the following cryptographic functions from crypto++:

  • HMAC for computing checksums of encrypted messages and pseudo-random functions. Please note that the standard HMAC implementation is used and not the one from I2P
  • SHA256 hash for use with HMAC and to calculate the checksum of all messages involved in establishing a connection
  • Functions for working with descriptions in ASN.1 language in DER encoding. Required to extract the public key from certificate X.509


A PKCS v1.5 based RSA implementation is used. The key length can be any and is determined by the certificate.


Message transmission over SSL


Absolutely all transmitted messages begin with a 5-byte header, the first byte of which contains the message type, the next 2 bytes - the protocol version number (0x03, 0x03 for TSL 1.2) and then the length of the remaining part (content) of the message - 2 bytes in Big Endian, so thereby defining the boundaries of messages.
Thus, when new data is received, 5 bytes of the header should be read first, and then how many bytes are contained in the length field.
There are 4 types of messages:
  1. 0x17 - data. The content is encrypted HTTP messages, in our case using AES256, the key of which is calculated during the connection establishment process. The data size must be a multiple of 16 bytes
  2. 0x16 - connection establishment. Several types, defined by the corresponding field within the content. Unencrypted, except for the 'finished' message sent last.
  3. 0x15 - warning. A message that “something went wrong.” We close the connection. Contains codes of what exactly went wrong, can be used for debugging.
  4. 0x14 - change the cipher. Sent immediately after key negotiation. The content is 1 byte, always containing 0x01. It is actually part of the connection setup process.


In our implementation, the encrypted data looks like this::
16 IV byte for CBC, in TSL 1.2 each message has its own IV;
data length up to 64K - length of headers;
32 MAC byte calculated for a 13-byte header and data, the header consisting of an 8-byte sequence number starting with zero, message type (0x17 or 0x16), version and data length. All in BigEndian. The key for HMAC is also calculated during connection setup;
filler, so that the length of the encrypted data is a multiple of 16 bytes, the last byte contains the number of bytes of the filler without taking into account the filler itself. If the message length is a multiple of 16 bytes, then another 16 bytes will be added for this last byte with the length.

Establishing a connection


During the installation process we must solve two problems:
  1. Negotiate and compute keys for encryption and HMAC
  2. Send the correct sequence of messages so that the other side does not close the connection, but switches to data exchange mode


In our case, the message sequence looks like this::
ClientHello--> (0x01)
<--ServerHello (0x02)
<--Certificate (0x0B)
<--ServerHelloDone(0x0E)
-->ClientKeyExchange (0x10)
-->ChangeCipherSpec
-->Finished (0x14)
<--ChangeCipherSpec
<--Finished (0x14)
where "--&gt;" means sending a message, and "&lt;--" means receiving.
All messages except ChangeChiperSpec are message type 0x16 - connection setup. The content of a message of this type begins with its own 4-byte header, the first byte of which is the connection setup message type as indicated above, and 3 bytes of the length of the remaining message, the high byte of which in our case is always zero.
Let's look at these messages in detail..

ClientHello

The first message that we send to the server after a successful connection. Since we are using one specific cipher suite, in our case it will be constant. Like this:
static uint8_t clientHello[] = 
{
	0x16, // handshake
	0x03, 0x03, // version (TLS 1.2)
	0x00, 0x2F, // length of handshake
	// handshake
	0x01, // handshake type (client hello)
	0x00, 0x00, 0x2B, // length of handshake payload 
	// client hello
	0x03, 0x03, // highest version supported (TLS 1.2)
	0x45, 0xFA, 0x01, 0x19, 0x74, 0x55, 0x18, 0x36, 
	0x42, 0x05, 0xC1, 0xDD, 0x4A, 0x21, 0x80, 0x80, 
	0xEC, 0x37, 0x11, 0x93, 0x16, 0xF4, 0x66, 0x00, 
	0x12, 0x67, 0xAB, 0xBA, 0xFF, 0x29, 0x13, 0x9E, // 32 random bytes
	0x00, // session id length
	0x00, 0x02, // chiper suites length
	0x00, 0x3D, // RSA_WITH_AES_256_CBC_SHA256
	0x01, // compression methods length
	0x00,  // no compression
	0x00, 0x00 // extensions length
};	


This message tells the server that we support TLS 1.2, this is a new connection (session ID length is zero), and we support the only cipher suite - RSA with AES256. We also transmit a set of 32 “random” bytes to generate keys. If these bytes are truly random, then they should be remembered somewhere, because they will be needed later.

ServerHello

«Twin brother of ClientHello, except the message type is 0x02 instead of 0x01, and the session ID is non-empty. From this message we only need 32 random bytes.

Certificate

Can contain several certificates, first comes the length of the entire group of certificates, then each certificate has its own length. We are only interested in the first certificate and the length should be read 2 times. The certificate itself is X.509 encoded DER. From it we need the RSA public key.

ServerHelloDone

Does not contain anything useful, but is taken into account when calculating the hash for Finished.

ClientKeyExchange

At this point, we have enough information to generate and agree on keys, which occurs in 3 stages: generating a random secret key, calculating the master key, expanding the master key to obtain encryption keys and checksum.
The random secret key is 48 bytes, the first 2 of which are the version number (0x03, 0x03), and the remaining 46 are randomly generated. Next, these 48 bytes are encrypted with an RSA public key and, together with the length of the encrypted block, are sent to the server. It should be noted that the length of the encrypted block will be equal to the length of the key, and not 48 bytes. For example, for certificates with a 2048-bit key, this length will be 256, and the length of the transmitted data — 258.

ChangeCipherSpec

Sent immediately after ClientKeyExchange. Always the same:
static uint8_t changeCipherSpecs[] =
{
	0x14, // change cipher specs
	0x03, 0x03, // version (TLS 1.2)
	0x00, 0x01, // length
	0x01 // type
};

This is a message of type 0x14 and the hash calculation for Finished is not involved.

Pseudo-random function (PRF)

To further calculate the keys, we need a pseudo-random function that takes 4 parameters as input: the secret key just sent to the server, a label in the form of a text string, a block of initial data and the desired length of the result.
In TLS 1.2 it is defined as follows:
PRF(secret, label, seed) = P_SHA256(secret, label + seed);
P_SHA256(secret, seed) = HMAC_SHA256(secret, A(1) + seed) +
HMAC_SHA256(secret, A(2) + seed) +
HMAC_SH256(secret, A(3) + seed) +…
where A is determined by induction
A(0) = seed,
A(i) = HMAC_SHA256 (secret, A(i -1)).
That is, at each step we recalculate the checksum from the previous step, and then calculate the checksum from combining the result with a text string and the original data, repeating this until the desired length is obtained.

Now the master key is calculated using the formula
PRF(secret, "master secret", clientRandom + serverRadom, 48);
where clientRandom is 32 random bytes from ClientHello and serverRandom is from ServerHello.
It should then be expanded to a 128-byte block containing 4 32-byte keys in the following sequence: send MAC key, receive MAC key, send encryption key, receive decryption key.
We do not use the MAC key for receiving.
The key expansion is made according to the formula
PRF(masterSecret, "key expansion", serverRandom + clientRadom, 128)
clientRadom and serverRadom are swapped here.

Finished

At this point we have everything we need to start exchanging data, but unfortunately we must send a Finished message containing the correct data, otherwise the server will close the connection.
If all the previous posts were quite trivial, then Finshed is more complex. Firstly, it is of type 0x16, but its contents are completely encrypted, and when calculating the checksum, 0x16 also appears, and not 0x17 as for other encrypted messages.
The message itself contains the first 12 bytes from
PRT(masterSecret, "client finished", hash, 12)
where hash is the SHA256 from the following sequence of messages:
ClientHello, ServerHello, Certficate, ServerHelloDone, ClientKeyExchange. All messages are counted without the 5 byte header.
If the message is formed correctly, the server will respond with ChangeCipherSpec and Finished, otherwise with an error message.
After this, the server is ready to exchange data and we send our HTTP request and receive a response.

conclusions


The approach discussed in the article allows you to effectively work with https for applications that do not require its full implementation. Instead of third-party ssl implementations that draw their own cryptography, you can use the one already present in the project, as shown in the crypto++ example, which reduces the number of dependencies, improves support and portability.
Implemented and used practically in i2pd — C++ implementations of I2P
Tags:
Hubs:
Всего голосов 20: ↑20 и ↓0 +20
Комментарии 12
+12

Comments 12

Thanks for the wonderful article)
Why is https better than http if you don't check the server certificate? What benefits does this provide??
And it doesn’t give any, but since the owners of those nodes began to switch to https, then I had to whether I wanted it or not. They began to switch because recently these nodes have been subjected to DDoS attacks - it’s clear that I2P has become a problem for someone and they mistake it for Tor, thus trying to either paralyze the network or collect a list of all nodes. They thought that if they switched to https, they would stop doing this. Didn't stop.
Those. the fact that the kind Vasya Spuffkin will give his own to the i2p client instead of your bootstrap list - you don’t care? :)
It won’t give it back because the su3 file cannot be signed with the key of any of the receivers.
Please tell us why the primitives you described are more secure than the primitives described by the author of this article in the absence of server authorization. So far, from my point of view, such a statement looks like “I know karate, kung fu and many other scary words.”».
When using Diffie-Hellman, it is impossible to decrypt data using a private key, but since authorization is not checked at all, it no longer matters, but for the encryption itself they could at least use more modern technologies.
Google Chrome hints
The Diffie-Hellman algorithm, of course, has nothing to do with what you are trying to describe (and you are trying to describe forward secrecy, apparently). The forward secrecy property can be achieved with RSA, but generating an ephemeral RSA key is, of course, a very expensive operation compared to generating a DH group.

Regarding GCM vs CBC-SHA1, I would like to note the following: if in the second scheme you replace SHA1 with the same SHA256/512, then the authentication scheme becomes quite adequate. But implementing GMAC in software, which would still be constant time, is a terribly difficult task (no joke). All AES-GCM implementations typically use PCLMULQDQ (intel core+) for Galois field multiplication, which requires a minimum of SSE2 instructions. Therefore, for embedded or minimalistic TLS implementations, I would recommend cbc-sha2 rather than GCM.
The Diffie-Hellman algorithm, of course, has nothing to do with what you are trying to describe (and you are trying to describe forward secrecy, apparently).
I wrote “ephemeral” and not static, which, by the way, Android, iOS and Mac OS support for some reason.
The forward secrecy property can be achieved with RSA, but generating an ephemeral RSA key is, of course, a very expensive operation compared to generating a DH group.
Well, yes, I read it on stackoverflow not long ago, and compared to ECDH it’s even slower ;).

Useful comments, write more, seriously.
The main problem with RSA in TLS is the growth rate:
                  sign    verify    sign/s verify/s
rsa  512 bits 0.000046s 0.000004s  21648.2 270106.4
rsa 1024 bits 0.000150s 0.000010s   6686.0  99103.0
rsa 2048 bits 0.001127s 0.000034s    887.6  29280.0
rsa 4096 bits 0.007963s 0.000127s    125.6   7893.4

In TLS, the server signs a random cookie to provide replay protection for the session. And as the complexity of the signature grows, it grows quadratically. In comparison, in ECC the complexity of the signature (using proper point multiplication algorithms such as Montgomery or Karatsuba stairs on some platforms and curves) grows almost linearly. Here are the results of nistp256 (similar to RSA 3072):
256 bit ecdh (nistp256): 5391.8 op/s
256 bit ecdsa(nistp256): 9483.1 signs/s


The proposed new standard for 128-bit curves (which assumes a 256-bit key in ECC) - curve25519 - is even faster, giving somewhere around 20k ecdh per second on the same machine.
Security is not needed - you need i2pseeds.su3.
Only full-fledged users can leave comments. Sign in, Please.